package com.polymathcoder.dpastry;

import com.polymathcoder.dpastry.api.Service;
import com.polymathcoder.dpastry.api.config.Configuration;
import com.polymathcoder.dpastry.api.config.ConfigurationException;
import com.polymathcoder.dpastry.api.environment.Environment;
import com.polymathcoder.dpastry.api.environment.FlexProcessor;
import com.polymathcoder.dpastry.api.environment.RandomSource;
import com.polymathcoder.dpastry.api.environment.TimeSource;
import com.polymathcoder.dpastry.core.config.CoreConfiguration;
import com.polymathcoder.dpastry.core.config.TcpTransportConfiguration;
import com.polymathcoder.dpastry.core.network.TcpTransportService;
import com.polymathcoder.dpastry.core.util.JavaRandomSource;
import com.polymathcoder.dpastry.core.util.JavaTimeSource;
import com.polymathcoder.dpastry.core.util.ServiceHandle;
import com.polymathcoder.dpastry.core.util.TaskProcessor;
import org.jboss.netty.util.internal.ExecutorUtil;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.ScheduledThreadPoolExecutor;
import java.util.concurrent.atomic.AtomicLong;

/**
 * dPastry - The primary entry point for a dPastry p2p connection.
 * <p/>
 * This class acts as a central bootstrap for all dPastry activities. p2p-based services can be
 * registered with dPastry instances and communicate with other dPastry nodes sharing the same services.
 */
public class dPastry implements Environment {

    private final Logger logger = LoggerFactory.getLogger(dPastry.class);

    // *************** PROTECTED FIELDS ***************

    protected final AtomicLong counter = new AtomicLong(0);//@TODO:evaluate auto-(un)boxing cost for long as hashMap key
    protected final ConcurrentHashMap<Long, Service> services = new ConcurrentHashMap<Long, Service>();

    protected final ExecutorService scheduledPool;

    protected ServiceHandleImpl<TcpTransportService> networkService;

    protected final CoreConfiguration cfg;

    protected TaskProcessor processingPool;
    protected TaskProcessor ioPool;

    protected RandomSource random;
    protected TimeSource time;

    // *************** CONSTRUCTORS ***************

    /**
     * Simple Constructor initializes a dPastry instance with the default configuration options.
     *
     * @throws com.polymathcoder.dpastry.api.config.ConfigurationException
     *          Configuration-specific exception containing the underlying cause of the issue
     */
    public dPastry() throws ConfigurationException {
        this(CoreConfiguration.getDefaultConfiguration());
    }

    /**
     * Initializes a dPastry instance with the given CoreConfiguration
     *
     * @param cfg the CoreConfiguration to use for this dPastry instance
     * @throws com.polymathcoder.dpastry.api.config.ConfigurationException
     *          Configuration-specific exception containing the underlying cause of the issue
     */
    public dPastry(CoreConfiguration cfg) throws ConfigurationException {
        if (cfg == null) {
            throw new ConfigurationException("Configuration Parameter cannot be null.");
        }
        // PREPARE CONFIGURATION
        this.cfg = cfg;
        // PREPARE ENVIRONMENT
        random = new JavaRandomSource();
        time = new JavaTimeSource();
        // PREPARE TASK PROCESSORS
        processingPool = new TaskProcessor(cfg.processingPool().getCorePoolSize(), cfg.processingPool().getMaxPoolSize(), cfg.processingPool().getKeepAliveTime());
        ioPool = new TaskProcessor(cfg.ioPool().getCorePoolSize(), cfg.ioPool().getMaxPoolSize(), cfg.ioPool().getKeepAliveTime());
        // PREPARE THREAD POOLS
        scheduledPool = new ScheduledThreadPoolExecutor(2);
        // PREPARE NETWORK AND CORE SERVICES
        try {
            networkService = addService(TcpTransportService.class);
            setTcpConfiguration(TcpTransportConfiguration.getDefaultConfiguration());
        } catch (IllegalAccessException e) {
            e.printStackTrace();//@TODO: decide best approach for this!
        } catch (InstantiationException e) {
            e.printStackTrace();//@TODO: decide best approach for this!
        }
    }

    // *************** SET-UP / CONFIGURATION METHODS ***************

    public boolean setTcpConfiguration(TcpTransportConfiguration cfg) throws ConfigurationException {
        if (!networkService.isAlive()) {//if the service is unreachable, we must fail!
            return false;
        }
        TcpTransportService s = networkService.get();
        if (s.isRunning()) {// we won't try to change the configuration of a running service!
            return false;//throw exception?!??
        }
        cfg.setHandle(networkService);// set cfg handle to correct handle before init!
        s.initialize(cfg);

//      Alternative Code for "Live" Configuration Changes! (probably not a good idea, but might be convenient...)
//        boolean doRestart = s.isRunning(); //we want to return the service to the state it started in!
//        //must ensure that service is stopped before changing config!
//        if (stopService(s)) {
//            s.initialize(cfg);
//        }
//        if (doRestart) {
//            return startService(s);
//        }
        return true;
    }

    public void setTimeSource(TimeSource source) {
        time = source;
    }

    public void setRandomSource(RandomSource source) {
        random = source;
    }

    // *************** ENVIRONMENT METHODS ***************

    @Override
    public Logger getLogger(Class clazz) {
        return LoggerFactory.getLogger(clazz);
    }

    @Override
    public Logger getLogger(String name) {
        return LoggerFactory.getLogger(name);
    }

    @Override
    public FlexProcessor processingPool() {
        return processingPool;
    }

    @Override
    public FlexProcessor secondaryPool() {
        return ioPool;
    }

    @Override
    public TimeSource time() {
        return time;
    }

    @Override
    public RandomSource random() {
        return random;
    }


    // *************** LIFE-CYCLE METHODS ***************

    public <T extends Service<C>, C extends Configuration<T>> ServiceHandleImpl<T> registerService(Class<T> clazz, C config)
            throws IllegalAccessException, InstantiationException, ConfigurationException {
        ServiceHandleImpl<T> s = addService(clazz);
        s.get().initialize(config);
        return s;
    }

    public void start() {
        //Start Network Service
        networkService.start();

        for (Service s : services.values()) {//@TODO: evaluate if services.values() causes a locking bottleneck (shouldn't)
            startService(s);
        }
    }

    public void stop() {
        networkService.stop();//@TODO: Create a priority(/dependency?) structure for services so that this can be stopped last!?

        for (Service s : services.values()) {
            stopService(s);
        }
    }

    public void shutdown() {
        stop();
        // Shutdown Services
        for (long i : services.keySet()) {//@TODO: evaluate if services.keySet() causes a locking bottleneck (shouldn't)
            destroyService(i, services.get(i));
        }
        // Shutdown Thread Pools
        ExecutorUtil.terminate(processingPool, ioPool, scheduledPool); // Thanks Trustin Lee & Netty! :-)
    }

    // *************** INTERNAL SERVICE LIFE-CYCLE METHODS ***************

    protected <T extends Service> ServiceHandleImpl<T> addService(Class<T> clazz) throws IllegalAccessException, InstantiationException {
        T newService = clazz.newInstance();
        Long index = counter.incrementAndGet();
        services.put(index, newService);
        return new ServiceHandleImpl<T>(this, index);
    }

    protected <T extends Service> boolean destroyService(Long index, T service) {
        if (stopService(service)) {
            service.destroy();
        } else {
            service.forceDestroy();
        }
        return services.remove(index, service);
    }

    protected <T extends Service> boolean destroyService(ServiceHandleImpl<T> handle) {
        return destroyService(handle.getKey(), handle.get());
    }

    protected <T extends Service> boolean startService(T s) {
        if (!s.isRunning()) {
            s.start();
            //do timeout here!
            long t = time.currentTimeMillis();
            while (time.currentTimeMillis() - t < cfg.serviceOptions().getStartTimeout() && !s.isRunning()) {
                Thread.yield();
            }
        }
        return s.isRunning();
    }

    protected <T extends Service> boolean startService(ServiceHandleImpl<T> handle) {
        return startService(handle.get());
    }

    protected <T extends Service> boolean stopService(T s) {
        if (s.isRunning()) {
            s.stop();
            //do timeout here!
            long t = time.currentTimeMillis();
            while (time.currentTimeMillis() - t < cfg.serviceOptions().getStopTimeout() && s.isRunning()) {
                Thread.yield();
            }
        }
        return !s.isRunning();
    }

    protected <T extends Service> boolean stopService(ServiceHandleImpl<T> handle) {
        return stopService(handle.get());
    }

    // *************** NESTED SERVICE HANDLE CLASS ***************

    public final static class ServiceHandleImpl<T extends Service> extends ServiceHandle<T, dPastry, Long> {

        public ServiceHandleImpl(dPastry container, Long i) {
            super(container, i);
        }

        @Override
        @SuppressWarnings("unchecked")
        protected T fetch() {
            return (T) container.services.get(key);
        }

        protected Long getKey() {
            return key;
        }

        public boolean isAlive() {
            return container.services.containsKey(key);
        }

        public boolean destroy() {
            return container.destroyService(this);
        }

        public boolean start() {
            return container.startService(this);
        }

        public boolean stop() {
            return container.stopService(this);
        }
    }

}
